#include "wm.h"
#include "input.h"
#include "display.h"

#include <nxos.h>

#if defined(CONFIG_WM_DRAW_CURSOR)
#include <gato/render.h>
#endif

NX_PRIVATE WM_WindowManager wm;
NX_PRIVATE int eventSoltCount;
NX_PRIVATE NX_Solt eventSolts[WM_POLL_EVENT_MAX];

NX_WindowID WM_WindowAllocID(WM_WindowType *w)
{
    NX_WindowID id = NX_WIN_ID_INVALID;

    for (NX_WindowID i = 0; i < NX_WIN_ID_MAX; i++)
    {
        if (wm.windowID[i] == NX_NULL)
        {
            id = i;
            w->id = id;
            wm.windowID[i] = w;
            break;
        }
    }
    return id;
}

void WM_WindowFreeID(NX_WindowID id)
{
    if (id < NX_WIN_ID_MAX)
    {
        if (wm.windowID[id])
        {
            wm.windowID[id] = NX_NULL;
        }
    }
}

WM_WindowType *WM_WindowGetByID(NX_WindowID id)
{
    if (id < NX_WIN_ID_MAX)
    {
        return wm.windowID[id];
    }
    return NX_NULL;
}

void WM_SendRenderEvent(WM_WindowRenderEvent e)
{
    NX_Size len;
    
    len = NX_FifoWrite(wm.base.event, (const NX_U8 *)&e, sizeof(WM_WindowRenderEvent));
    if (!len)
        NX_Printf("write len:%d\n", len);
}

void WM_SendKeycode(NX_WindowEventType type, int code)
{
    if (wm.focused)
    {
        /* send quit event */
        if (type == NX_KEYDOWN_EVENT && (code == NX_KEY_HOME || code == NX_KEY_ESCAPE))
        {
            WM_WindowSendEvent(
                wm.focused,
                (NX_WindowEvent){
                    .type = NX_QUIT_EVENT
                });
        }

        WM_WindowSendEvent(
            wm.focused,
            (NX_WindowEvent){
                .type = type,
                .keydown_event = {
                    .code = code,
                    .modify = g_wm_input.key_modify,
                },
            });
    }
}

void WM_MergeUpdateRegion(NX_Region r)
{
    wm.update = NX_RegionMerge(r, wm.update);
}

void WM_FlushWindow(void)
{
    WM_WindowType *win;

    if (NX_RegionIsEmpty(wm.update))
        return;

    NX_SurfaceClear(wm.base.surface, wm.base.bg, wm.update.x, wm.update.y, wm.update.w, wm.update.h);

    NX_ListForEachEntry(win, &wm.windowList, node)
    {
        NX_SurfaceBlitRegionXY(wm.base.surface, win->surface, wm.update, win->config.x, win->config.y);
    }

    /* copy surface to fb */
    WM_DisplayPresent(wm.base.surface, &wm.update);

    /* signal to all wait thread */
    NX_ListForEachEntry(win, &wm.windowList, node)
    {
        if (win->flushSync == WM_flushSync_RECVED)
        {
            NX_SemaphoreSignal(win->updateSem);
            win->flushSync = WM_flushSync_DONE;
        }
    }

    wm.update = (NX_Region){0, 0, 0, 0};
}

NX_Error WM_WindowSendEvent(WM_WindowType * w, NX_WindowEvent e)
{
    int len;

    len = NX_FifoWrite(w->event, (const NX_U8 *)&e, sizeof(NX_WindowEvent));
    if (len <= 0)
    {
        return NX_ENORES;
    }
    else
    {
        return NX_EOK;
    }
}

NX_Error WM_WindowRecvEvent(WM_WindowType * w, NX_WindowEvent *e, NX_Bool isBlock)
{
    int len;

    if (isBlock)
    {
        while((len = NX_FifoRead(w->event, (const NX_U8 *)e, sizeof(NX_WindowEvent))) <= 0)
        {
            NX_ThreadYield();
        }
    }
    else
    {
        len = NX_FifoRead(w->event, (const NX_U8 *)e, sizeof(NX_WindowEvent));
    }

    if (len <= 0)
    {
        return NX_ENORES;
    }
    else
    {
        return NX_EOK;
    }
}

void WM_RaiseWindow(WM_WindowType * w)
{
    if (wm.cursor != NX_NULL)
    {
        /* add window before cursor */
        NX_ListDelInit(&w->node);
        NX_ListAddTail(&w->node, &wm.cursor->node);
    }
    else
    {
        NX_ListDelInit(&w->node);
        NX_ListAddTail(&w->node, &wm.windowList);
    }
}

NX_PRIVATE void ShowWindow(WM_WindowType * w)
{
    WM_RaiseWindow(w);
    WM_MergeUpdateRegion((NX_Region){w->config.x, w->config.y, w->surface->width, w->surface->height});
}

NX_PRIVATE void MoveWindow(WM_WindowType * w, int dx, int dy)
{
    NX_Region r1 = {w->config.x, w->config.y, w->surface->width, w->surface->height};
    NX_Region r2 = {w->config.x + dx, w->config.y + dy, w->surface->width, w->surface->height};
    w->config.x += dx;
    w->config.y += dy;
    WM_MergeUpdateRegion(NX_RegionMerge(r1, r2));
}

NX_PRIVATE WM_WindowType * GetTopWindow(void)
{
    WM_WindowType *w = NX_NULL;
    NX_ListForEachEntryReverse(w, &wm.windowList, node)
    {
        if (w != wm.cursor)
        {
            return w;
        }
    }

    return NX_NULL;
}

NX_PRIVATE WM_WindowType * IsWindowHit(int x, int y)
{
    WM_WindowType *w = NX_NULL;
    NX_ListForEachEntryReverse(w, &wm.windowList, node)
    {
        if (NX_RegionHit((NX_Region){w->config.x, w->config.y, w->surface->width, w->surface->height}, x, y))
        {
            if (w != wm.cursor)
            {
                return w;
            }
        }
    }

    return NX_NULL;
}

NX_PRIVATE WM_WindowType * ClickWindow(int x, int y)
{
    WM_WindowType * w = IsWindowHit(x, y);

    if (w != NX_NULL)
    {
        ShowWindow(w);
    }

    return w;
}

void WM_HideWindow(WM_WindowType * w)
{
    NX_ListDelInit(&w->node);
}

void WM_FocusWindowByTop(void)
{
    wm.focused = GetTopWindow();
}

void WM_FocusWindow(WM_WindowType * w)
{
    wm.focused = w;
}

void WM_FocusWindowByMouse(int mouse_x, int mouse_y, int code)
{
    wm.focused = ClickWindow(mouse_x, mouse_y);
    if (wm.focused != NX_NULL)
    {
        int x, y;

        x = mouse_x - wm.focused->config.x;
        y = mouse_y - wm.focused->config.y;
        
        /* check move area */
        if (code == NX_BTN_MOUSE_LEFT)
        {
            wm.is_press = NX_True;
        }

        WM_WindowSendEvent(
            wm.focused,
            (NX_WindowEvent){
                .type = NX_MOUSEBUTTONDOWN_EVENT,
                .mouse_buttondown_event = {
                    .x = x,
                    .y = y,
                    .code = code,
                },
            });
    }    
}

void WM_ReleaseMouseOnFocusWindow(int mouse_x, int mouse_y, int code)
{
    if (wm.focused != NX_NULL)
    {
        int x, y;

        x = mouse_x - wm.focused->config.x;
        y = mouse_y - wm.focused->config.y;
        
        /* check move area */
        if (code == NX_BTN_MOUSE_LEFT)
        {
            wm.is_press = NX_False;
        }

        WM_WindowSendEvent(
            wm.focused,
            (NX_WindowEvent){
                .type = NX_MOUSEBUTTONUP_EVENT,
                .mouse_buttondown_event = {
                    .x = x,
                    .y = y,
                    .code = code,
                },
            });
    }
}

void WM_WheelMouseOnFocusWindow(int mouse_x, int mouse_y, NX_WindowEventType type)
{
    if (wm.focused != NX_NULL)
    {
        WM_WindowSendEvent(
            wm.focused,
            (NX_WindowEvent){
                .type = type,
                .mouse_wheel_event = {
                    .x = mouse_x - wm.focused->config.x,
                    .y = mouse_y - wm.focused->config.y,
                },
            });
    }
}

NX_PRIVATE void MoveCursor(WM_WindowType * w, int x, int y)
{
    if (x >= 0 && x < wm.base.surface->width && y >= 0 && y < wm.base.surface->height)
    {
        NX_Region r1 = {w->config.x, w->config.y, w->surface->width, w->surface->height};
        NX_Region r2 = {x, y, w->surface->width, w->surface->height};

        w->config.x = x;
        w->config.y = y;
        WM_MergeUpdateRegion(NX_RegionMerge(r1, r2));
    }
}

NX_PRIVATE NX_Error WM_RenderPoll(void)
{
    if (NX_FifoLength(wm.base.event) > 0)
    {
        WM_WindowRenderEvent re;
        WM_WindowType * w;
        int ret = NX_FifoRead(wm.base.event, (const NX_U8 *)&re, sizeof(WM_WindowRenderEvent));
        if (ret <= 0)
        {
            return NX_ENORES;
        }
        
        if (re.type == WM_RENDER_UPDATE)
        {
            WM_MergeUpdateRegion(re.update_event.region);

            w = WM_WindowGetByID(re.update_event.wid);
            if (w)
            {
                w->flushSync = WM_flushSync_RECVED;      
            }
        }


        return NX_EOK;
    }
    return NX_ENORES;
}

NX_PRIVATE void HandleEvent(void)
{
    /* fix mouse position */
    g_wm_input.mouse_x = NX_CLAMP(g_wm_input.mouse_x, 1, wm.base.surface->width - 1);
    g_wm_input.mouse_y = NX_CLAMP(g_wm_input.mouse_y, 1, wm.base.surface->height - 1);

    /* get mouse change info */
    int real_dx = g_wm_input.mouse_x - g_wm_input.prev_mouse_x;
    int real_dy = g_wm_input.mouse_y - g_wm_input.prev_mouse_y;

    if ((real_dx || real_dy))
    {
        /* check window hit */
        WM_WindowType * win = IsWindowHit(g_wm_input.prev_mouse_x, g_wm_input.prev_mouse_y);
        if (win != NX_NULL)
        {
            /* move hit window */
            if (wm.is_press == NX_True)
            {
                //MoveWindow(win, real_dx, real_dy);
            }

            /* send mouse abs event to click window */
            if (win->config.event_mask & NX_ABS_EVENT)
            {
                WM_WindowSendEvent(
                    win,
                    (NX_WindowEvent){
                        .type = NX_ABS_EVENT,
                        .abs_event = {
                            .x = g_wm_input.mouse_x - win->config.x,
                            .y = g_wm_input.mouse_y - win->config.y,
                            .x_root = g_wm_input.mouse_x,
                            .y_root = g_wm_input.mouse_y,
                        },
                    });
            }
        }

        {
            /* send mouse postion to all window */
            WM_WindowType * tmp;
            NX_ListForEachEntry(tmp, &wm.windowList, node)
            {
                if (tmp->config.event_mask & NX_ROOT_ABS_EVENT)
                {
                    WM_WindowSendEvent(
                        tmp,
                        (NX_WindowEvent){
                            .type = NX_ROOT_ABS_EVENT,
                            .root_abs_event = {
                                .x_root = g_wm_input.mouse_x,
                                .y_root = g_wm_input.mouse_y,
                            },
                        });
                }
            }
        }

        /* check window move out */
        if (wm.hit != NX_NULL && wm.hit != win)
        {
            WM_WindowSendEvent(wm.hit, (NX_WindowEvent){.type = NX_MOUSE_OUT_EVENT});
        }

        /* check window move in */
        if (win != NX_NULL && wm.hit != win)
        {
            WM_WindowSendEvent(win, (NX_WindowEvent){.type = NX_MOUSE_IN_EVENT});
        }

        /* update hit window */
        wm.hit = win;

        /* move cursor finally */
        MoveCursor(wm.cursor, g_wm_input.mouse_x, g_wm_input.mouse_y);
    }
}

void WM_PollEventInit(void)
{
    eventSoltCount = 0;

    eventSolts[eventSoltCount++] = wm.base.event;

    if (g_wm_input.kbd_solt != NX_SOLT_INVALID_VALUE)
    {
        eventSolts[eventSoltCount++] = g_wm_input.kbd_solt;
    }
    if (g_wm_input.mouse_solt != NX_SOLT_INVALID_VALUE)
    {
        eventSolts[eventSoltCount++] = g_wm_input.mouse_solt;
    }
    if (g_wm_input.joystick_solt != NX_SOLT_INVALID_VALUE)
    {
        eventSolts[eventSoltCount++] = g_wm_input.joystick_solt;
    }

    NX_Printf("wm: poll event count:%d\n", eventSoltCount);
}

NX_Error WM_PollEvent(void)
{
    NX_PollState pState;
    NX_Error err;

    pState.inmask = NX_POLL_READ;

    if ((err = NX_PollWait(eventSolts, eventSoltCount, 50, &pState)) == NX_EOK)
    {
        if (pState.solt == g_wm_input.kbd_solt)
        {
            return WM_InputRead(g_wm_input.kbd_solt);
        }
        else if (pState.solt == g_wm_input.mouse_solt)
        {
            return WM_InputRead(g_wm_input.mouse_solt);
        }
        else if (pState.solt == wm.base.event)
        {
            return WM_RenderPoll();
        }
        else if (pState.solt == g_wm_input.joystick_solt)
        {
            return WM_InputRead(g_wm_input.joystick_solt);
        }
        else
        {
            return NX_EPERM;
        }
    }
    return NX_ENORES;
}

NX_PRIVATE void WM_LoopEvent()
{
    while (1)
    {
        /* update mouse info */
        g_wm_input.mouse_x = g_wm_input.prev_mouse_x;
        g_wm_input.mouse_y = g_wm_input.prev_mouse_y;
        
        /* poll event */
        if (WM_PollEvent() != NX_EOK)
        {
            NX_ThreadYield();
        }

        HandleEvent();

        /* update prev mouse postion */
        g_wm_input.prev_mouse_x = g_wm_input.mouse_x;
        g_wm_input.prev_mouse_y = g_wm_input.mouse_y;

        /* flush windows after all events */
        WM_FlushWindow();
    }
}

NX_PRIVATE NX_Error WindowInit(WM_WindowType * w, int x, int y, int width, int height, NX_Color bg, NX_U32 flags)
{
    NX_Color *pixel;

    if (!w || width <= 0 || height <= 0)
    {
        return NX_EINVAL;
    }

    w->event = NX_FifoCreate(sizeof(NX_WindowEvent) * 64);
    if (!w->event)
    {
        NX_MemFree(w);
        return NX_ENOMEM;
    }

    pixel = (NX_Color *)NX_MemAlloc(width * height * sizeof(NX_Color));
    if (!pixel)
    {
        NX_FifoDestroy(w->event);
        NX_MemFree(w);
        return NX_ENOMEM;
    }

    if (WM_WindowAllocID(w) < 0)
    {
        NX_FifoDestroy(w->event);
        NX_MemFree(pixel);
        NX_MemFree(w);
        return NX_ENORES;
    }

    w->config.x = x;
    w->config.y = y;
    w->config.width = width;
    w->config.height = height;
    w->config.event_mask = NX_EVENT_MASK_DEFAULT;
    w->bg = bg;

    NX_ListInit(&w->node);

    NX_SurfaceWrap(w->surface, pixel, width, height);
    NX_SurfaceClear(w->surface, w->bg, 0, 0, width, height);

    return NX_EOK;
}

NX_PRIVATE NX_Error WindowDeinit(WM_WindowType * w)
{
    if (!w)
    {
        return NX_EINVAL;
    }
    NX_FifoDestroy(w->event);
    NX_SurfaceFree(w->surface);
    WM_WindowFreeID(w->id);
    return NX_EOK;
}

NX_PRIVATE WM_WindowType * WindowCreate(int x, int y, int width, int height, NX_Color bg, NX_U32 flags)
{
    WM_WindowType * w;
    NX_Error err;

    if (width <= 0 || height <= 0)
    {
        return NX_NULL;
    }

    w = NX_MemAlloc(sizeof(WM_WindowType));
    if (!w)
    {
        return NX_NULL;
    }
    
    if ((err = WindowInit(w, x, y, width, height, bg, flags)) != NX_EOK)
    {
        NX_Printf("init window failed with err %d!", err);
        return NX_NULL;
    }
    
    return w;
}

NX_PRIVATE NX_Error WindowDestroy(WM_WindowType * w)
{
    if (!w)
    {
        return NX_EINVAL;
    }
    WindowDeinit(w);
    NX_MemFree(w);
    return NX_EOK;
}

NX_U32 WM_GetWidth(void)
{
    return wm.base.config.width;
}

NX_U32 WM_GetHeight(void)
{
    return wm.base.config.height;
}

NX_U32 WM_GetHeight(void);

NX_PRIVATE NX_Error InitCursor(int x, int y)
{
    WM_WindowType *w = WindowCreate(x, y, 16, 25, NX_ColorRGB(0xffffff), 0);
    if (!w)
    {
        return NX_ENOMEM;
    }

#if defined(CONFIG_WM_DRAW_CURSOR)
    NX_SurfaceClear(w->surface, NX_ColorARGB(0), 0, 0, w->surface->width, w->surface->height);
    draw_polygon((surface_t *)w->surface, (point_t[]){
        {0, 0}, {14.2, 14.2}, {8.4, 14.3}, {12.9, 23.1}, {9.6, 25.0}, {4.6, 15.8}, {0, 19.4}},
        7,
        (style_t){
        fill_color : RGB(0xffffff),
        stroke_color : RGB(0),
        stroke_width : 2,
    });
#endif

    ShowWindow(w);
    wm.cursor = w;
    return NX_EOK;
}

NX_PRIVATE NX_Error ExitCursor(void)
{
    WindowDestroy(wm.cursor);
    return NX_EOK;
}

NX_PRIVATE NX_Error WindowManagerInit(void)
{
    NX_Error err;
    NX_Surface * surface;
    int width, height;

    width = WM_DisplayGetWidth();
    height = WM_DisplayGetHeight();

    if ((err = WindowInit(&wm.base, 0, 0, width, height, NX_ColorRGB(0x404040), 0)) != NX_EOK)
    {
        NX_Printf("init window failed with err %d!\n", err);
        return NX_ENOMEM;
    }

    surface = wm.base.surface;

    g_wm_input.prev_mouse_x = surface->width / 2, g_wm_input.prev_mouse_y = surface->height / 2;

    wm.update = (NX_Region){0, 0, surface->width, surface->height};
    NX_ListInit(&wm.windowList);
    wm.focused = NX_NULL;
    wm.hit = NX_NULL;
    wm.is_press = NX_False;

    /* init cursor */
    if ((err = InitCursor(g_wm_input.prev_mouse_x, g_wm_input.prev_mouse_y)) != NX_EOK)
    {
        WindowDeinit(&wm.base);
        return err;
    }

    return NX_EOK;
}

NX_PRIVATE void WindowManagerExit(void)
{
    ExitCursor();
    WindowDeinit(&wm.base);
}

void WM_WindowSystemExit(void)
{
    WindowManagerExit();
    WM_InputExit();
    WM_DisplayExit();
}

void WM_SignalHandler(NX_SignalInfo * si)
{
    NX_ProcessExit(si->signal);
}

NX_Error WM_WindowSystemInit(void)
{
    NX_Error err;

    /* init display */
    if ((err = WM_DisplayInit()) != NX_EOK)
    {
        return err;
    }

    /* init input */
    if ((err = WM_InputInit()) != NX_EOK)
    {
        goto failed_input;
    }

    /* init window manager */
    if ((err = WindowManagerInit()) != NX_EOK)
    {
        goto failed_wm;
    }

    WM_PollEventInit();

    /* exit call */
    // NX_ExitCall(WM_WindowSystemExit);

    NX_SignalAttr attr;
    NX_SignalInitAttr(&attr, WM_SignalHandler, 0);
    NX_SignalSetAttr(NX_SIGNAL_TERMINATE, &attr);

    WM_LoopEvent();
    return NX_EOK;

failed_wm:
    WM_InputExit();
failed_input:
    WM_DisplayExit();
    return err;
}
